#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import json
import time

import parent
from testlib import *


@skipDistroPackage()
class HostSwitcherHelpers:

    def check_discovered_addresses(self, b, addresses):
        b.click("button:contains('Add new host')")
        b.wait_popup('hosts_setup_server_dialog')
        self.wait_discovered_addresses(b, addresses)
        b.click('#hosts_setup_server_dialog .pf-m-link')
        b.wait_popdown('hosts_setup_server_dialog')

    def wait_discovered_addresses(self, b, expected):
        b.wait_js_cond('ph_select("#hosts_setup_server_dialog datalist option").length == {0}'.format(len(expected)))
        # Check that we rendered all expected hosts
        for address in expected:
            b._wait_present("#hosts_setup_server_dialog datalist option[value='{0}']".format(address))

    def wait_host_addresses(self, b, expected):
        b.wait_js_cond('ph_select("#nav-hosts .nav-item a").length == {0}'.format(len(expected)))
        for address in expected:
            b.wait_visible("#nav-hosts .nav-item a[href='/@{0}']".format(address))

    def machine_remove(self, b, address, machine, second_to_last=False):
        b.click("button:contains('Edit hosts')")
        b.click(".nav-item span[data-for='/@{0}'] button.nav-action.pf-m-danger".format(address))
        if second_to_last:
            b.wait_not_present("button:contains('Stop editing hosts')")
            b.wait_not_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-danger")
        else:
            b.click("button:contains('Stop editing hosts')")

        # Wait until all related iframes are gone
        b.wait_js_func("""(function (dropped) {
          const frames = document.getElementsByTagName("iframe");
          for (i = 0; i < frames.length; i++)
            if (frames[i].getAttribute['data-host'] === dropped)
              return false;
          return true;
        })""", address)

    def add_machine(self, b, address, known_host=False):
        b.click("button:contains('Add new host')")
        b.wait_popup('hosts_setup_server_dialog')
        b.set_val('#add-machine-address', address)
        self.add_machine_finish(b, address, known_host=known_host)

    def add_machine_finish(self, b, address, known_host=False):
        b.click('#hosts_setup_server_dialog .pf-m-primary:contains("Add")')
        if not known_host:
            b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to %s for the first time" % address)
            b.click('#hosts_setup_server_dialog .pf-m-primary')
        b.wait_popdown('hosts_setup_server_dialog')

    def connect_and_wait(self, b, address):
        b.click("a[href='/@{0}']".format(address))
        b.click("#hosts-sel button")
        b.wait_visible(".connected a[href='/@{0}']".format(address))
        # Switch back to localhost, since the rest of the test expects that
        b.click("a[href='/@localhost']")
        b.click("#hosts-sel button")

    def get_pubkey(self, machine, account):
        return machine.execute("cat /home/%s/.ssh/id_rsa.pub" % account)

    def authorize_pubkey(self, machine, account, pubkey):
        machine.execute("a=%s d=/home/$a/.ssh; mkdir -p $d; chown $a:$a $d; chmod 700 $d" % account)
        machine.write("/home/%s/.ssh/authorized_keys" % account, pubkey)
        machine.execute("a=%s; chown $a:$a /home/$a/.ssh/authorized_keys" % account)

    def setup_ssh_auth(self):
        self.machine.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")
        self.machine.execute("test -f /home/admin/.ssh/id_rsa || ssh-keygen -f /home/admin/.ssh/id_rsa -t rsa -N ''")
        self.machine.execute("chown admin:admin /home/admin/.ssh/id_rsa*")
        pubkey = self.get_pubkey(self.machine, "admin")

        for m in self.machines:
            self.authorize_pubkey(self.machines[m], "admin", pubkey)


@no_retry_when_changed
@skipDistroPackage()
class TestHostSwitching(MachineCase, HostSwitcherHelpers):
    provision = {
        'machine1': {"address": "10.111.113.1/20"},
        'machine2': {"address": "10.111.113.2/20"},
        'machine3': {"address": "10.111.113.3/20"}
    }

    def setUp(self):
        super().setUp()

        # Disable preloading on all machines ("machine1" is done in testlib.py)
        # Preloading on machines with debug build can overload the browser and cause slowness and browser crashes
        # In these tests we actually switch between machines in quick succession which can make things even worse
        if self.is_devel_build():
            for machine in ["machine2", "machine3"]:
                for pkg in ["packagekit", "systemd", "playground"]:
                    self.machines[machine].write(f"/usr/share/cockpit/{pkg}/override.json", '{ "preload": [ ] }')
        # Also, quick logouts cause async preloads to run into "ReferenceError: cockpit is not defined"
        self.disable_preload("packagekit", "playground", "systemd")

        self.machines["machine1"].execute("hostnamectl set-hostname localhost")
        self.machines["machine2"].execute("hostnamectl set-hostname machine2")
        self.machines["machine3"].execute("hostnamectl set-hostname machine3")
        self.setup_ssh_auth()

        # removing machines interrupts channels
        self.allow_restart_journal_messages()
        self.allow_hostkey_messages()

    def testBasic(self):
        b = self.browser
        m2 = self.machines["machine2"]
        m3 = self.machines["machine3"]

        m2.execute("hostnamectl set-hostname machine2")
        m3.execute("hostnamectl set-hostname machine3")

        # This should all work without being admin on machine1
        self.login_and_go(superuser=False)

        b.click("#hosts-sel button")
        self.wait_host_addresses(b, ["localhost"])

        b.wait_not_present("button:contains('Edit hosts')")

        self.add_machine(b, "10.111.113.2")
        self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
        self.connect_and_wait(b, "10.111.113.2")

        # Main host should have both buttons disabled, the second both enabled
        b.click("button:contains('Edit hosts')")
        b.wait_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-danger:disabled")
        b.wait_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-secondary:disabled")
        b.wait_visible(".nav-item span[data-for='/@10.111.113.2'] button.nav-action.pf-m-danger:not(:disabled)")
        b.wait_visible(".nav-item span[data-for='/@10.111.113.2'] button.nav-action.pf-m-secondary:not(:disabled)")
        b.click("button:contains('Stop editing hosts')")
        b.wait_not_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-danger")
        b.wait_not_visible(".nav-item span[data-for='/@10.111.113.2'] button.nav-action.pf-m-secondary")

        b.wait_not_present(".nav-item span[data-for='/@10.111.113.2'] .nav-status")

        self.add_machine(b, "10.111.113.3")
        self.wait_host_addresses(b, ["localhost", "10.111.113.3", "10.111.113.2"])
        self.connect_and_wait(b, "10.111.113.3")

        # Remove two
        self.machine_remove(b, "10.111.113.2", m2)
        self.wait_host_addresses(b, ["localhost", "10.111.113.3"])

        self.machine_remove(b, "10.111.113.3", m3, True)
        self.wait_host_addresses(b, ["localhost"])

        # Check that the two removed machines are listed in "Add Host"
        self.check_discovered_addresses(b, ["10.111.113.2", "10.111.113.3"])

        # Add one back, check addresses on both browsers
        self.add_machine(b, "10.111.113.2", True)
        self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
        self.connect_and_wait(b, "10.111.113.2")
        self.check_discovered_addresses(b, ["10.111.113.3"])

        b.wait_not_present(".nav-item span[data-for='/@10.111.113.2'] .nav-status")

        # And the second one, check addresses
        self.add_machine(b, "10.111.113.3", True)
        self.wait_host_addresses(b, ["localhost", "10.111.113.2", "10.111.113.3"])
        self.connect_and_wait(b, "10.111.113.3")
        self.check_discovered_addresses(b, [])

        # Test change user, not doing in edit to reuse machines

        # Navigate to load iframe
        b.click("#nav-hosts .nav-item a[href='/@10.111.113.3']")
        b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.3/system']")

        b.click("#hosts-sel button")
        b.click("button:contains('Edit hosts')")

        b.click("#nav-hosts .nav-item span[data-for='/@10.111.113.3'] button.nav-action.pf-m-secondary")

        b.wait_popup('hosts_setup_server_dialog')
        b.set_val('#add-machine-user', 'bad-user')
        b.click('#hosts_setup_server_dialog .pf-m-primary')
        b.wait_in_text("#hosts_setup_server_dialog", "Unable to log in to")
        b.click('#hosts_setup_server_dialog button:contains("Cancel")')
        b.wait_popdown('hosts_setup_server_dialog')

        # Test switching
        b.wait_js_cond('ph_select("#nav-hosts .nav-item a").length == 3')

        b.click("#nav-hosts .nav-item a[href='/@localhost']")
        b.wait_js_cond('window.location.pathname == "/system"')

        b.click("#hosts-sel button")
        b.click("#nav-hosts .nav-item a[href='/@10.111.113.2']")
        b.wait_js_cond('window.location.pathname.indexOf("/@10.111.113.2") === 0')

        b.click("#hosts-sel button")
        b.click("#nav-hosts .nav-item a[href='/@10.111.113.3']")
        b.wait_js_cond('window.location.pathname.indexOf("/@10.111.113.3") === 0')

        b.enter_page("/system", "10.111.113.3")
        b.wait_text_not("#system_information_systime_button", "")
        b.click(".system-information a")  # View hardware details
        b.enter_page("/system/hwinfo", "10.111.113.3")
        b.click(".pf-c-breadcrumb li:first-child a")
        b.enter_page("/system", "10.111.113.3")

        # Remove host underneath ourselves
        b.switch_to_top()
        b.click("#hosts-sel button")
        b.click("button:contains('Edit hosts')")
        b.click("#nav-hosts .nav-item span[data-for='/@10.111.113.3'] button.nav-action.pf-m-danger")
        b.wait_not_present("iframe.container-frame[name='cockpit1:10.111.113.3/network']")
        b.wait_js_cond('window.location.pathname == "/system"')
        b.enter_page("/system", "localhost")

        self.allow_journal_messages(".*server offered unsupported authentication methods: password public-key.*")

    def testBasicAsAdmin(self):
        b = self.browser
        m2 = self.machines["machine2"]
        m3 = self.machines["machine3"]

        # When being admin, changes in the host switcher are supposed
        # to be reflected in all browser sessions.

        self.login_and_go()

        b.click("#hosts-sel button")
        self.wait_host_addresses(b, ["localhost"])

        b.wait_not_present("button:contains('Edit hosts')")

        # Start second browser and check that it is in sync
        b2 = self.new_browser()
        b2.default_user = "admin"
        b2.login_and_go()

        b2.click("#hosts-sel button")
        self.wait_host_addresses(b2, ["localhost"])

        self.add_machine(b, "10.111.113.2")
        self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
        self.wait_host_addresses(b2, ["localhost", "10.111.113.2"])
        self.connect_and_wait(b, "10.111.113.2")
        self.connect_and_wait(b2, "10.111.113.2")

        # Main host should have both buttons disabled, the second both enabled
        b.click("button:contains('Edit hosts')")
        b.wait_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-danger:disabled")
        b.wait_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-secondary:disabled")
        b.wait_visible(".nav-item span[data-for='/@10.111.113.2'] button.nav-action.pf-m-danger:not(:disabled)")
        b.wait_visible(".nav-item span[data-for='/@10.111.113.2'] button.nav-action.pf-m-secondary:not(:disabled)")
        b.click("button:contains('Stop editing hosts')")
        b.wait_not_visible(".nav-item span[data-for='/@localhost'] button.nav-action.pf-m-danger")
        b.wait_not_visible(".nav-item span[data-for='/@10.111.113.2'] button.nav-action.pf-m-secondary")

        b.wait_not_present(".nav-item span[data-for='/@10.111.113.2'] .nav-status")

        self.add_machine(b, "10.111.113.3")
        self.wait_host_addresses(b, ["localhost", "10.111.113.3", "10.111.113.2"])
        self.wait_host_addresses(b2, ["localhost", "10.111.113.3", "10.111.113.2"])
        self.connect_and_wait(b, "10.111.113.3")
        self.connect_and_wait(b2, "10.111.113.3")

        # Remove two
        self.machine_remove(b, "10.111.113.2", m2)
        self.wait_host_addresses(b, ["localhost", "10.111.113.3"])
        self.wait_host_addresses(b2, ["localhost", "10.111.113.3"])

        self.machine_remove(b, "10.111.113.3", m3, True)
        self.wait_host_addresses(b, ["localhost"])
        self.wait_host_addresses(b2, ["localhost"])

        # Check that the two removed machines are listed in "Add Host"
        # on both browsers
        self.check_discovered_addresses(b, ["10.111.113.2", "10.111.113.3"])
        self.check_discovered_addresses(b2, ["10.111.113.2", "10.111.113.3"])

        # Add one back, check addresses on both browsers
        self.add_machine(b, "10.111.113.2", True)
        self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
        self.wait_host_addresses(b2, ["localhost", "10.111.113.2"])
        self.connect_and_wait(b, "10.111.113.2")
        self.check_discovered_addresses(b, ["10.111.113.3"])
        self.check_discovered_addresses(b2, ["10.111.113.3"])

        b.wait_not_present(".nav-item span[data-for='/@10.111.113.2'] .nav-status")

        # And the second one, check addresses on both browsers
        self.add_machine(b, "10.111.113.3", True)
        self.wait_host_addresses(b, ["localhost", "10.111.113.2", "10.111.113.3"])
        self.wait_host_addresses(b2, ["localhost", "10.111.113.2", "10.111.113.3"])
        self.connect_and_wait(b, "10.111.113.3")
        self.check_discovered_addresses(b, [])
        self.check_discovered_addresses(b2, [])

    def testEdit(self):
        b = self.browser
        m1 = self.machines['machine1']
        m2 = self.machines['machine2']
        m3 = self.machines['machine3']

        m2.execute("hostnamectl set-hostname machine2")
        m3.execute("hostnamectl set-hostname machine3")

        self.allow_journal_messages("Could not chdir to home directory /home/franz: No such file or directory")
        m1.execute("useradd franz")
        m1.execute("echo franz:foobar | chpasswd")
        m3.execute("useradd franz")
        m3.execute("echo franz:foobar | chpasswd")
        self.authorize_pubkey(m3, "franz", self.get_pubkey(m1, "admin"))

        # This should all work without being admin on m1
        self.login_and_go(superuser=False)

        b.click("#hosts-sel button")
        self.add_machine(b, "10.111.113.3")
        self.wait_host_addresses(b, ["localhost", "10.111.113.3"])
        self.connect_and_wait(b, "10.111.113.3")

        b.click("button:contains('Edit hosts')")
        b.click("#nav-hosts .nav-item span[data-for='/@10.111.113.3'] button.nav-action.pf-m-secondary")

        b.wait_popup('hosts_setup_server_dialog')
        old_machine3_color = b.attr("#host-edit-color", "style")
        b.set_val('#add-machine-user', 'franz')
        b.click('#host-edit-color')
        b.wait_visible('#host-edit-color-popover')
        b.click('#host-edit-color-popover div.popover-content > div:nth-child(3)')
        b.wait_not_visible('#host-edit-color-popover')
        new_machine3_color = b.attr("#host-edit-color", "style")
        self.assertNotEqual(old_machine3_color, new_machine3_color)

        b.click('#hosts_setup_server_dialog .pf-m-primary')
        b.wait_popdown('hosts_setup_server_dialog')

        b.wait_text(".nav-item span[data-for='/@10.111.113.3']", "franz @machine3")

        # Go to the updated machine and try to change whilst on it
        b.click("#nav-hosts .nav-item a[href='/@10.111.113.3']")
        b.wait_visible("iframe.container-frame[name='cockpit1:franz@10.111.113.3/system']")

        b.wait_text("#hosts-sel button .pf-c-select__toggle-text", "franz@machine3")
        b.click("#hosts-sel button")
        b.wait_text(".nav-item span[data-for='/@10.111.113.3']", "franz @machine3")
        b.click("button:contains('Edit hosts')")
        b.click("#nav-hosts .nav-item span[data-for='/@10.111.113.3'] button.nav-action.pf-m-secondary")

        b.wait_val('#add-machine-address', "10.111.113.3")
        self.assertEqual(b.attr("#host-edit-color", "style"), new_machine3_color)
        b.wait_val('#add-machine-user', 'franz')
        b.set_val('#add-machine-address', "10.111.113.2")
        self.assertNotEqual(b.attr("#host-edit-color", "style"), new_machine3_color)
        b.set_val('#add-machine-user', 'admin')
        b.click('#hosts_setup_server_dialog .pf-m-primary')
        b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
        b.click('#hosts_setup_server_dialog .pf-m-primary')
        b.wait_popdown('hosts_setup_server_dialog')

        b.wait_text("#hosts-sel button .pf-c-select__toggle-text", "admin@machine2")

        # Changing the address of a host will navigate to that host,
        # and that will close the host switcher.  Let's open it again
        # to check it.
        b.click("#hosts-sel button")
        b.wait_not_present(".nav-item span[data-for='/@10.111.113.3']")
        b.wait_text(".nav-item span[data-for='/@10.111.113.2']", "admin @machine2")

    def testNoAutoconnect(self):
        b = self.browser
        m2 = self.machines["machine2"]

        self.login_and_go(None)

        # And and connect to a second machine
        b.click("#hosts-sel button")
        self.add_machine(b, "10.111.113.2")
        b.click("a[href='/@10.111.113.2']")
        b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.2/system']")
        self.assertIn("admin", m2.execute("loginctl"))
        b.click("#hosts-sel button")
        b.click("a[href='/@localhost']")
        b.relogin()
        time.sleep(60)
        self.assertNotIn(m2.execute("loginctl"), "admin")


@skipDistroPackage()
class TestHostEditing(MachineCase, HostSwitcherHelpers):

    def testLimits(self):
        b = self.browser
        m = self.machine

        def fake_machines(amount):
            # build a machine json manually
            d = {
                "localhost": {"visible": True, "address": "localhost"}
            }

            for i in range(amount):
                n = "bad{0}".format(i)
                d[n] = {"visible": True, "address": n}

            m.execute("echo '{0}' > /etc/cockpit/machines.d/99-webui.json".format(json.dumps(d)))
            return list(d.keys())

        def check_limit(limit):
            b.click("button:contains('Add new host')")
            b.wait_popup('hosts_setup_server_dialog')
            if limit:
                b.wait_in_text("#hosts_setup_server_dialog .dashboard-machine-warning",
                               "{0} machines".format(limit))
            else:
                b.wait_not_present("#hosts_setup_server_dialog .dashboard-machine-warning")

            b.click("#hosts_setup_server_dialog .pf-m-link")

        self.login_and_go(superuser=False)

        b.click("#hosts-sel button")
        self.wait_host_addresses(b, ["localhost"])
        check_limit(0)

        self.wait_host_addresses(b, fake_machines(3))
        check_limit(0)

        self.wait_host_addresses(b, fake_machines(14))
        check_limit(20)
        self.allow_journal_messages(
            ".*couldn't connect: Failed to resolve hostname bad.*",
            ".*refusing to connect to unknown host.*"
        )


if __name__ == '__main__':
    test_main()
